#! /usr/bin/python2.4

# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""check_owners.py unittest"""


import unittest

import svn.fs
import svn.repos

import gvn.hooks.info
from gvn.hooks.pre_commit.check_owners import RunHook

import testing.hooks.common

SUPER_GROUP = "super-group"


"""
Our testdata/test-checkin1 tree looks like this:
test-checkin1
test-checkin1/file1
test-checkin1/B
test-checkin1/B/dir
test-checkin1/B/dir/fileB_dir
test-checkin1/B/fileB
test-checkin1/C
test-checkin1/C/fileC
test-checkin1/D
test-checkin1/D/fileD

The relevant portion of public/projects/greek-tree looks like this:

public/projects/greek-tree/ +
 OWNERS < "user1 \n group: group1"
 A
 A/B
 A/B/OWNERS < "set noparen \n userAB \n userAB2"
 A/B/E
 A/B/F
 A/C/OWNERS < "file" (unsupported for now)
 A/D
 A/D/OWNERS < "groupAD" (missing "group:", so looks like a username)
 A/D/G
 A/D/G/OWNERS < "bad owners syntax ADG" (ignored)

Depending on which piece of it we try to check in, we trigger different
allow rules. See below for which users belong to which groups.
"""

class TestOwners(testing.hooks.common.HookTestCase):

  def setUp(self):
    # HookTestCase creates and sets a _logger object based on whether
    # GVN_TEST_DEBUG is exported or not
    testing.hooks.common.HookTestCase.setUp(self)

    # create group pickle to use as our group file
    user_groups= {
      "user1": [ "group1", "group2", "group3" ],
      "user2": [ "group1", "group2" ],
      "user3": [ "group1" ],
      "user4": [  ],
      "user5": [ "group5" ],
      "userAB2": [ "group1" ],
      "userAD": [ "groupAD" ],
      "adminuser": [ SUPER_GROUP ],
    }

    for user in user_groups:
      self.userdb.WriteUserGroups(user, user_groups[user])

  def testNoOwner(self):
    """check a commit outside of OWNERS controlled tree.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/dir", "user1"), 0)

  def testGoodOwner(self):
    """check an OWNERs commit with correct owner.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "user1"), 0)

  def testUserNotInOwner(self):
    """check an OWNERS commit with incorrect owner.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "baduser"),
                          "baduser not authorized in any OWNERS file")

  def testGroupOwner(self):
    """check an OWNERS commit with a group allowed owner (group1).
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "user2"), 0)

  def testBadGroupOwner(self):
    """check an OWNERS commit with an owner in the group file without perms.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "user5"),
                          "user5 not authorized in any OWNERS file")

  def testInheritGoodOwner(self):
    """check an inherited OWNERs commit with correct owner.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A", "user1"), 0)

  def testInheritGroupOwner(self):
    """check an inherited OWNERS commit with a group allowed owner (group1).
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A", "user2"), 0)

  def testBadOwnerInherit(self):
    """check an inherited OWNERS commit with a disallowed owner.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A", "baduser"),
                          "baduser not authorized in any OWNERS file")

  def testBadGroupOwnerInherit(self):
    """check an inherited OWNERS commit with a group file user without perms.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A", "user5"),
                          "user5 not authorized in any OWNERS file")

  def testNoParentGoodOwner(self):
    """check an OWNER inside a file with noparent.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A/B", "userAB"), 0)

  def testNoParentOwner(self):
    """check that noparent works for denying users higher up.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A/B", "user1"),
                          "user1 not authorized in any OWNERS file")

  def testNoParentGroupOwner(self):
    """check that noparent works for denying groups higher up.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A/B", "user2"),
                          "user2 not authorized in any OWNERS file")

  def testBadGroupSyntax(self):
    """check that group OWNERShip is not granted by group without group prefix.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A/D", "userAD"),
                          "userAD not authorized in any OWNERS file")

  def testBadOwnerSyntaxGoodUser(self):
    """check that a syntax error in OWNERS doesn't reject an otherwise ok user.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A/D/G", "user1"), 0)


  def testBadOwnerSyntaxBadUser(self):
    """check that a syntax error in OWNERS doesn't mistakenly allow a user.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree/A/D/G", "userAD"),
                          "userAD not authorized in any OWNERS file")

  def testGvnApprove(self):
    """check an OWNERS commit with incorrect owner but with correct approve.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "baduser", "user1"), 0)

  def testGvnApproveSuperuser(self):
    """check that superuser can approve even though not in OWNERS"""
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "baduser",
                                          "adminuser"),
                          0)

  def testBadSuperuser(self):
    """check that superuser can't commit when not in OWNERS"""
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                                          "public/projects/greek-tree",
                                          "adminuser", approve_user=None),
                          "adminuser not authorized in any OWNERS file")

  def testBadGvnApprove(self):
    """check an OWNERS submit with incorrect owner but with incorrect approve.
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata",
                          "public/projects/greek-tree", "baduser", "user4"),
                          "baduser not authorized in any OWNERS file")

  def testMixedAllowDisallow(self):
    """do a mixed allowed/disallow submit: user2 ok in /, but not in A/B
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata/test-checkin1",
                          "public/projects/greek-tree/A", "user2"),
                          "user2 not authorized in any OWNERS file")

  def testMixedAllowDisallow2(self):
    """do a mixed allowed/disallow submit: userAB ok in / & A/B, but not in A/D
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata/test-checkin1",
                          "public/projects/greek-tree/A", "userAB"),
                          "userAB not authorized in any OWNERS file")

  def testMixedAllow(self):
    """do a dual allow with different files allowed by different OWNERS files
    """
    self.assertHookResult(self._TestPaths(RunHook, "testdata/test-checkin1",
                          "public/projects/greek-tree/A", "userAB2"), 0)


  def testGoodOwnerProp(self):
    """check an OWNERs prop set with correct owner and no upper restriction
    """
    self.assertHookResult(self._TestProp(RunHook, "proptest", "propvalue",
                          "user1", "public/projects/greek-tree"), 0)

  def testPropUserNotInOwner(self):
    """check an OWNERS propset with incorrect owner.
    """
    self.assertHookResult(self._TestProp(RunHook, "proptest", "propvalue",
                          "baduser", "public/projects/greek-tree"),
                          "baduser not authorized in any OWNERS file")

  def testPropUserInOwner(self):
    """check an OWNERS propset with correct owner.
    """
    self.assertHookResult(self._TestProp(RunHook, "proptest", "propvalue",
                          "userAB", "public/projects/greek-tree/A/B"), 0)

  def testPropUserInParentOwner(self):
    """check propset with correct owner for the parent but not current dir.
       
       This test is a bit tricky, when setting properties on a directory, we
       have special code that checks the OWNERS in that same directory and not
       OWNERS in the parent trees.
       This is why this test is meant to fail.
    """
    self.assertHookResult(self._TestProp(RunHook, "proptest", "propvalue",
                          "user1", "public/projects/greek-tree/A/B"),
                          "user1 not authorized in any OWNERS file")

  def testSnapshotWithOwners(self):
    """check that a non-OWNER of a project can still snapshot from it

    This is to avoid the following bug:
      0. trunk/OWNERS forbids me from modifying trunk
      1. create changebranch from trunk, mail for approval
         - //changes/epg/foo/trunk does not exist, so no OWNERS, so allow
      2. gvn snap more changes to my changebranch
         - //changes/epg/foo/trunk *does* exist, and so does
           //changes/epg/foo/trunk/OWNERS, so I get locked out!
    """
    # Mock up a HookInfo.
    class mock_userdb:
      def UserInGroup(self, user, group):
        return False
    hi = gvn.hooks.info.HookInfo(['pre-commit', 'unused-repo', 'unused-txn'],
                                 userdb=mock_userdb())
    hi._author = 'basil'
    hi._head_root = object()
    hi._txn = object()
    # Mock svn.fs.node_prop for hi.project_config's gvn:project search.
    def mock_node_prop(root, path, propname, pool=None):
      return None
    # Mock svn.fs.txn_prop for RunHook's gvn:change check.
    def mock_txn_prop(txn, propname, pool=None):
      return None
    # Mock svn.fs.check_path for RunHook.
    def mock_check_path(root, path, pool=None):
      if path.endswith('OWNERS'):
        return svn.core.svn_node_file
      # Our test modification path is a directory.
      return svn.core.svn_node_dir
    # Mock svn.core.Stream and svn.fs.file_contents to return an
    # OWNERS list excluding our intrepid hacker, basil.
    class mock_Stream:
      def __init__(self, data): self.data = data
      def read(self): return self.data
    def mock_file_contents(root, path, pool=None):
      return 'sybil\n'
    class mock_path_change_t:
      change_kind = svn.fs.path_change_modify
    # Save the originals...
    orig_node_prop = svn.fs.node_prop
    orig_txn_prop = svn.fs.txn_prop
    orig_check_path = svn.fs.check_path
    orig_Stream = svn.core.Stream
    orig_file_contents = svn.fs.file_contents
    try:
      # ...and install the mocks.
      svn.fs.node_prop = mock_node_prop
      svn.fs.txn_prop = mock_txn_prop
      svn.fs.check_path = mock_check_path
      svn.core.Stream = mock_Stream
      svn.fs.file_contents = mock_file_contents
      # First, check that basil cannot submit to the project.
      hi._paths_changed = {'/trunk': mock_path_change_t()}
      self.assertEqual(RunHook(hi, self._logger), 'basil not authorized in'
                       ' any OWNERS file for trunk, sorry')
      # Reset for next test.
      hi._prefix_changed = None
      # Now, check that basil can snapshot it.
      hi._paths_changed = {'/changes/basil/test/trunk': mock_path_change_t()}
      self.assertEqual(RunHook(hi, self._logger), 0)
    finally:
      svn.fs.node_prop = orig_node_prop
      svn.fs.txn_prop = orig_txn_prop
      svn.fs.check_path = orig_check_path
      svn.core.Stream = orig_Stream
      svn.fs.file_contents = orig_file_contents
